[Chapter 20] 20.2 The Cookbook
Author: Bryan Costales & Eric Allman
Date: 1999-01-06T19:24:37Z

#

| Chapter 20 The checkcompat() Cookbook |

## 20.2 The Cookbook

In this section we show several examples of possible uses for the checkcompat() routine. Among those we illustrate are the following:

- Accept mail only from our domain. (see Section 20.2.1, "Accept Mail Only From Our Domain").

- Cause your workstation to refuse to work as a mail gateway (see Section 20.2.2, "Workstation Refuses to Act as a Mail Gateway").

- Limit the size of guest account messages (see Section 20.2.3, "Limit the Size of Guest Messages").

- Verify that identd information is correct (see Section 20.2.4, "Verify identd Information").

- Prune `Received:` headers at a firewall (see Section 20.2.5, "Prune Received: Headers at Firewall").

- Reject mail from spamming or mail-bombing sites (see Section 20.2.6, "Reject Mail from Spamming or Mail-bombing Sites").

Note that in all of the following examples the numbers to the left indicate line numbers for discussion and are not a part of the code.

### 20.2.1 Accept Mail Only From Our Domain

If your site lives behind a firewall, [2] you might want to use checkcompat() to configure the internal sendmail so that it accepts only mail that is generated locally. The external sendmail (outside the firewall or part of it) acts as a proxy. That is, it accepts external mail that is destined for internal delivery from the outside and forwards it to the internal sendmail. Because the external sendmail is part of the local domain, its envelope always appears to be local. Any external mail that somehow bypasses the firewall needs to be bounced. The way to do this in checkcompat() looks like this:

> [2] A firewall is a machine that lies between the local network and the outside world. It intercepts and filters all network traffic and rejects any that are considered inappropriate.

>     # define OUR_NET_IN_HEX 0x7b2d4300 /* 123.45.67.0 in hex */
>     # define OUR_NETMASK 0xffffff00

>     checkcompat(to, e)
>           register ADDRESS *to;
>           register ENVELOPE *e;
>     {
>           if (tTd(49, 1))
>                   printf("checkcompat(to=%s, from=%s)\n",
>                           to->q_paddr, e->e_from.q_paddr);

>           if (RealHostAddr.sa.sa_family == 0)
>           {
>                   /* this is a locally submitted message */
>                   return EX_OK;
>           }
>           if (RealHostAddr.sa.sa_family != AF_INET ||
>           (RealHostAddr.sin.sin_addr.s_addr & OUR_NETMASK)!= OUR_NET_IN_HEX)
>           {
>                   usrerr("553 End run mail not allowed");
>                   e->e_flags |= EF_NO_BODY_RETN;
>                   to->q_status = "5.7.1";
>                   return (EX_UNAVAILABLE);
>           }
>           return (EX_OK);
>     }

The `usrerr`() routine (line 21) causes a warning to be printed at the sending site, and returning EX_UNAVAILABLE (line 24) causes the mail message to be bounced. Bounced mail is sent back to the originating sender. A copy may also be sent to the local postmaster depending on the setting of `PostmasterCopy` (`P`) option (see Section 34.8.46, PostmasterCopy (P)).

The EF_NO_BODY_RETN (line 22) causes only the headers from the message to be returned in bounced mail, not the original message body. Other envelope flags of interest can be found in Table 37.3 of Section 37.5.12.

The `to->q_status` (line 23) conveys the DSN error status in the bounced mail message (see RFC1893). Here, `5.7.1` indicates a permanent failure (`5`) of policy status (`7`), where delivery is not authorized and the message is refused (`1`).

Also note that this code sample is only a suggestion. It doesn't take into account that `RealHostAddr` may contain `0x7f000001` (127.0.0.1 for localhost).

### 20.2.2 Workstation Refuses to Act as a Mail Gateway

If you've spent many months getting your workstation set up and running perfectly, you might not want outsiders using it as a knowledgeable mail relay. One way to prevent such unwanted use is to set up checkcompat() in conf.c so that it rejects any mail from outside your machine that is destined to another site outside your machine. A desirable side effect is that this will also prevent outsiders from directly posting into your internal mailing lists.

>     checkcompat(to, e)
>             register ADDRESS *to;
>             register ENVELOPE *e;
>     {

>             if (tTd(49, 1))
>                     printf("checkcompat(to=%s, from=%s)\n",
>                             to->q_paddr, e->e_from.q_paddr);

>             if (RealHostAddr.sa.sa_family == 0)
>             {
>                     /* this is a locally submitted message */
>                     return (EX_OK);
>             }
>             /* only accept local delivery from outside */
>             if (!bitnset(M_LOCALMAILER, to->q_mailer->m_flags))
>             {
>                     usrerr("553 External gateway use prohibited");
>                     e->e_flags |= EF_NO_BODY_RETN;
>                     to->q_status = "5.7.1";
>                     return (EX_UNAVAILABLE);
>             }
>             return (EX_OK);
>     }

Although `to` (line 16) is really a linked list of recipients, we check only the current recipient to prevent spurious warnings. This is done because checkcompat() is called once for every recipient. The check in line 16 is to see whether `F=l` delivery agent flag is not set (see Section 30.8.28, F=l (lowercase L)) thus implying that the recipient is not local.

Note that this form of rejecting messages will not work on a mail hub. In that case more sophisticated checks need to be made. Among them are the following:

- Check all the IP domains for your site. If you have only one, the check in Section 20.2.1 will work. If you have several (as in an assortment of class `C` domains), the check will be more complex. If the connecting host is in your domain or one of your domains, you should accept the message.

- The envelope sender's host (`e->e_from->q_host`) should be checked to see whether it is in the class `$=w` (see Section 32.5.8, $=w). You can use the wordinclass() routine (see Section 20.3.8, wordinclass()) to look it up. If it is in `$=w`, you should accept the message. This prevents a message from being forwarded through a workstation.

- If the delivery agent for a recipient is `*include*`, the message is destined for a mailing list. You might wish to screen further at this point.

### 20.2.3 Limit the Size of Guest Messages

Suppose your site has reserved uids numbered from 900 to 999 for guest users. Because guests are sometimes inconsiderate, you might want to limit the size of their messages and the number of simultaneous recipients they may specify. One way to do this is with the checkcompat() routine:

>     #define MAXGUESTSIZE 8000
>     #define MAXGUESTNRCP 4

>     checkcompat(to, e)
>             register ADDRESS *to;
>             register ENVELOPE *e;
>     {

>             if (tTd(49, 1))
>                     printf("checkcompat(to=%s, from=%s)\n",
>                             to->q_paddr, e->e_from.q_paddr);

>             /* does q_uid contain a valid uid? - no external */
>             if (! bitset(QGOODUID, e->e_from.q_flags))
>                     return (EX_OK);
>             if (e->e_from.q_uid < 900 || e->e_from.q_uid > 999)
>                     return (EX_OK);
>             if (e->e_msgsize > MAXGUESTSIZE)
>             {
>                     syslog(LOG_NOTICE,
>                             "Guest %s attempted to send %d size",
>                             e->e_from.q_user, e->e_msgsize);
>                     usrerr("553 Message too large, %d max", MAXGUESTSIZE);
>                     e->e_flags |= EF_NO_BODY_RETN;
>                     to->q_status = "5.7.1";
>                     return (EX_UNAVAILABLE);
>             }
>             if (e->e_nrcpts > MAXGUESTNRCP)
>             {
>                     syslog(LOG_NOTICE,
>                             "Guest %s attempted to send %d recipients",
>                             e->e_from.q_user, e->e_nrcpts);
>                     usrerr("553 Too many recipients for guest, %d max",
>                             MAXGUESTNRCP);
>                     e->e_flags &= ~EF_NO_BODY_RETN;
>                     to->q_status = "5.7.1";
>                     return (EX_UNAVAILABLE);
>             }
>             return (EX_OK);
>     }

Note that `q_uid` will have a valid uid (QGOODUID will be set) only if the sender is local (line 14). For external mail coming in, QGOODUID will be clear.

Also note that we specifically do not return the message body (EF_NO_BODY_RETN) if the message was returned because it was too large (line 24). But we do return the message body if the message was rejected for too many recipients (line 35). Other envelope flags of interest can be found in Table 37.3 of Section 37.5.12.

### 20.2.4 Verify identd Information

When an outside host connects to the local sendmail via SMTP, its hostname is saved in the `$s` macro (see Section 31.10.33, $s). If the `Timeout.ident` option (see Section 34.8.70, Timeout (r)) is nonzero, sendmail uses the RFC1413 identification protocol to record the identity of the host at the other end, that is, the identity of the host that made the connection. That identity is recorded in the `$_` macro (see Section 31.10.1, $-).

If you are unusually picky about the identity of other hosts, you may wish to confirm that the host in `$s` is the same as the host in `$_`. One way to perform such a check is with the checkcompat() routine:

>     checkcompat(to, e)
>             register ADDRESS *to;
>             register ENVELOPE *e;
>     {
>             char *s, *u, *v;
>             int len;
>             static char old_s[MAXHOSTNAMELEN];

>             if (tTd(49, 1))
>                     printf("checkcompat(to=%s, from=%s)\n",
>                             to->q_paddr, e->e_from.q_paddr);

>             /* if $s is localhost or in $=w, accept it */
>             if ((s = macvalue('s', e)) == NULL)
>                     return (EX_OK);
>             if (strncasecmp(s, old_s, MAXHOSTNAMELEN-1) == 0)
>                     return (EX_OK);
>             else
>                     (void)sprintf(old_s, "%.*s", MAXHOSTNAMELEN-1, s);
>             if (strcasecmp(s, "localhost") == 0)
>                     return (EX_OK);
>             if (wordinclass(s, 'w') == TRUE)
>                     return (EX_OK);

>             if ((u = macvalue('_', e)) == NULL)
>                     return (EX_OK);
>             if ((u = strchr(u, '@')) == NULL)
>                     return (EX_OK);
>             if ((v = strchr(u, ' ')) != NULL)
>                     *v = ' ';
>             len = strlen(u);
>             if (v != NULL)
>                     *v = ' ';

>             if (strncasecmp(s, u, len) != 0)
>             {
>                  auth_warning(e, "$s=%s doesn't match $_=%.*s", s, len, u);
>             }
>             return (EX_OK);
>     }

First (line 16) we check to see whether we have already checked this value of `$s`. If so, we don't check again because checkcompat() is called once for each recipient. If `$s` is new, we save a copy of its value for next time.

Then we make sure that the local host (no matter what its name) is acceptable (lines 20 and 22). If this is an offsite host, we compare the values of `$s` and the host part of `$_` (line 35). If they don't match, we insert an `X-Authentication-Warning:` header (line 37). This keeps such warnings under the control of the `PrivacyOptions.authwarnings` (`p`) option (see Section 34.8.47, PrivacyOptions (p)).

### 20.2.5 Prune Received: Headers at Firewall

In routing mail outward from a firewall (see Section 20.2.1), it may be advantageous to replace all the internal `Received:` headers with one master header. A way to do this with checkcompat() looks like this:

>     # define OUR_NET_IN_HEX 0x7b2d4300 /* 123.45.67.0 in hex */
>     # define OUR_NETMASK 0xffffff00
>     # define LOOP_CHECK "X-Loop-Check"

>     checkcompat(to, e)
>             register ADDRESS *to;
>             register ENVELOPE *e;
>     {
>             HDR *h;
>             int cnt;

>             if (RealHostAddr.sa.sa_family == 0)
>             {
>                     /* this is a locally submitted message */
>                     return EX_OK;
>             }
>             if (RealHostAddr.sa.sa_family != AF_INET ||
>             (RealHostAddr.sin.sin_addr.s_addr & OUR_NETMASK) != OUR_NET_IN_HEX)
>             {
>                     /* not received from the internal network */
>                     return EX_OK;
>             }
>             if (hvalue(LOOP_CHECK, e->e_header) != NULL)
>             {
>                     /* We've stripped them once already */
>                     return EX_OK;
>             }
>             addheader(LOOP_CHECK, "", &e->e_header);

>             for (cnt = 0, h = e->e_header; h != NULL; h = h->h_link)
>             {
>                    if (strcasecmp(h->h_field, "received") != 0)
>                            continue;
>                    if (cnt++ == 0)
>                            continue;
>                    clrbitmap(h->h_mflags);
>                    h->h_flags |= H_ACHECK;
>             }
>             return (EX_OK);
>     }

Because we are stripping the message of `Received:` headers, we need to be careful. We shouldn't do it if the message originated on the firewall machine (line 12). We also shouldn't do it if the message originated from outside the internal (firewalled) network (lines 17 and 18). To prevent possibly disastrous mail loops, we check for a special header (line 23) and skip stripping again if that header is found. We then add that special header (line 120), just in case the mail flows though this firewall again.

If it is okay to do so, we scan all the headers (line 30) looking for all `Received:` headers (line 32). We skip deleting the first one because it was placed there by the firewall (line 34). We delete all the others by clearing their `?``flags``?` bits (line 36) and setting the H_ACHECK flag (line 37). See Section 20.3.3, for a general discussion of this technique.

Be aware that this is only one possible approach and that, depending on what other hosts on the Internet do to the message, this loop detection may break. A safer but more difficult approach is to rewrite the `Received:` headers themselves and to mask out sensitive information in them.

### 20.2.6 Reject Mail from Spamming or Mail-bombing Sites

As the Internet grows, your site may become more and more subject to advertising and vengeful attacks from the outside. Advertising attacks are called "spams" and are symptomized by advertisers sending multiple copies of advertisements through your internal mail lists or to several of your users. Vengeful attacks are called "mail bombs" and usually are detected by your mail spool directory filling with a huge number of messages from a single sender. [3]

> [3] Often in response to one of your users sending an offensive spam.

To limit your vulnerability to such events (and to others of a similar nature that may be invented in the future), you may screen mail from outside hosts using a combination of a database and checkcompat(). First we show you how to set up such a database, then we show you a checkcompat() routine for using it. [4]

> [4] You may also screen sender addresses at the SMTP MAIL command with the new V8.8 `check_mail` rule set (see Section 29.10.1, "The check_mail Rule Set"). Although it can be easier to design `check_mail` rules, the checkcompat() routine can be more powerful.

The source file for the database will look like this:

>     user@spam.host      spam
>     user@bomb.host      bomb

Here, each left-hand side entry is an email address with a user part, an `@`, and a host part. We will be screening on the basis of individual sender addresses rather than screening at a sitewide level. The right-hand side is either the word `spam` to represent a spamming sender or `bomb` to represent a mail-bombing sender.

If the source file is called /etc/mail/blockusers, the database will be created like this:

>     % makemap hash /etc/mail/blockusers.db < /etc/mail/blockusers

Here, we create a `hash` db style database. For other available styles, see Section 33.2, "Create Files with makemap".

Once the database is in place, your configuration file needs to be told of its existence. To do that, we use the `K` configuration command (see Section 33.3, "The K Configuration Command"):

>     Kbadusers hash -o /etc/mail/blockusers.db

For the m4 configuration technique you would place this declaration under the LOCAL_CONFIG line in your mc file (see Section 19.6.30, LOCAL-CONFIG).

One possible checkcompat() routine to handle all this will look like this:

>     checkcompat(to, e)
>             register ADDRESS *to;
>             register ENVELOPE *e;
>     {
>             STAB *map;
>             char *p;
>             int  ret = 0;

>             map = stab("badusers", ST_MAP, ST_FIND);
>             if (map == (STAB *)NULL)
>                     return (EX_OK);
>             p = (*map->s_map.map_class->map_lookup)
>                      (&map->s_map, e->e_from.q_paddr, NULL, &ret);
>             if (p == NULL)
>                     return (EX_OK);

>             if (strcasecmp(p, "spam") == 0)
>             {
>                     usrerr("553 Spamming mail rejected from %s",
>                             e->e_from.q_paddr);
>                     to->q_status = "5.7.1";
>                     return (EX_UNAVAILABLE);
>             }
>             if (strcasecmp(p, "bomb") == 0)
>             {
>                     usrerr("553 Message rejected from mail-bomber %s",
>                             e->e_from.q_paddr);
>                     e->e_flags &= ~EF_NO_BODY_RETN;
>                     to->q_status = "5.7.1";
>                     return (EX_UNAVAILABLE);
>             }
>             return (EX_OK);
>     }

Here we first look up the database named `badusers` in the symbol table (line 9). It is okay for the database not to exist (line 11). If the database exists, we look up the sender's address in it (line 12). If the address is not found, all is okay (line 15).

If the address was found in the database, we have a potential bad person. So we first check to see whether the address was marked as a `spam` (line 17). If it was, we bounce it with an appropriate error message (line 19).

We also bounce the message if it is a mail bomb (line 24). This is fraught with risk however. The bounced mail can fill up the outgoing queue, thereby accomplishing the bomber's ends in a different way. A better approach might be to drop the mail on the floor (see dropenvelope() in envelope.c), but we leave this as an exercise for the reader.

20.1 How checkcompat() Works |  | 20.3 Alphabetized V8.8 Subroutines